package net.cassite.daf4j.jpa;

import net.cassite.daf4j.DataUtils;
import net.cassite.daf4j.IData;
import net.cassite.daf4j.PreResult;
import net.cassite.daf4j.QueryParameterTypes;
import net.cassite.daf4j.ds.DSUtils;
import net.cassite.daf4j.ds.EntityDataParser;
import net.cassite.daf4j.ds.ParserPacket;
import net.cassite.daf4j.types.*;
import net.cassite.daf4j.util.Location;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;

/**
 * 将实体和IData实例转化为JPQL
 */
public class JPQLEntityDataParser implements EntityDataParser<JPQLContext, String> {
        @Override
        public String parseData(JPQLContext jpqlContext, IData<?> iData) {
                return parseEntity(jpqlContext, iData.getEntity()) + "." + DataUtils.findFieldNameByIData(iData);
        }

        @Override
        public String parseEntity(JPQLContext jpqlContext, Object o) {
                Location loc = findEntity(o, jpqlContext);
                return jpqlContext.aliasMap.get(loc);
        }

        @Override
        public String parsePlain(JPQLContext jpqlContext, Object o) {
                return "?" + jpqlContext.constantMap.add(o);
        }

        @Override
        public <DS> String parseQuery(JPQLContext jpqlContext, PreResult<?> preResult, ParserPacket<JPQLContext, String, DS> parserPacket) throws Exception {
                JPQLContext cont = jpqlContext.copyForSubQuery(preResult.entity, preResult.parameter == null || !preResult.parameter.parameters.containsKey(QueryParameterTypes.distinct));
                String res = DSUtils.listGenerationProcess(cont, preResult.whereClause, preResult.parameter, parserPacket).generalJPQL.toString();
                jpqlContext.aliasMap.setAliasCount(cont.aliasMap.getAliasCount());
                return res;
        }

        /**
         * 获取实体的调用位置<br>
         * 使用广度优先搜索<br>
         * 找到时自动加入AliasMap
         *
         * @param entity          从该实体开始寻找
         * @param toFind          要寻找的实体
         * @param list            已经经过的路径
         * @param args            上下文
         * @param alreadySearched 已经寻找过的实体
         * @return 找到的实体位置(Location对象), 没找到则返回null
         */
        private static Location findEntity(Object entity, Object toFind, List<String> list, JPQLContext args, Set<Object> alreadySearched, List<List<String>> locationsMightBeJoined) {
                Map<Object, String> objectStringMap = new LinkedHashMap<Object, String>();
                try {
                        for (Field f : entity.getClass().getFields()) {
                                // is IData
                                if (IData.class.isAssignableFrom(f.getType())) {
                                        // get 1st generic type
                                        Class<?> cls;
                                        try {
                                                cls = ((Class<?>) ((ParameterizedType) f.getGenericType()).getActualTypeArguments()[0]);
                                        } catch (ClassCastException e) {
                                                // not generic type , maybe one of XTypes
                                                Class<?> type = f.getType();
                                                if (XInt.class.isAssignableFrom(type)) {
                                                        cls = Integer.class;
                                                } else if (XLong.class.isAssignableFrom(type)) {
                                                        cls = Long.class;
                                                } else if (XDouble.class.isAssignableFrom(type)) {
                                                        cls = Double.class;
                                                } else if (XDate.class.isAssignableFrom(type)) {
                                                        cls = Date.class;
                                                } else if (XString.class.isAssignableFrom(type)) {
                                                        cls = String.class;
                                                } else if (XBool.class.isAssignableFrom(type)) {
                                                        cls = Boolean.class;
                                                } else {
                                                        throw e;
                                                }
                                        }

                                        // is not java. / javax.
                                        if (!cls.getName().startsWith("java.") && !cls.getName().startsWith("javax.")) {
                                                IData<?> data = (IData<?>) f.get(entity);
                                                Object obj = data.get();
                                                if (obj == null) continue;

                                                if (obj instanceof Iterable) {
                                                        // is aggregate
                                                        Iterator<?> it = ((Iterable<?>) obj).iterator();
                                                        if (it.hasNext()) {
                                                                Object o = it.next();
                                                                List<String> tmp = new ArrayList<String>(list);
                                                                tmp.add(f.getName());
                                                                locationsMightBeJoined.add(new ArrayList<String>(tmp));
                                                                if (o == toFind) {
                                                                        list.add(f.getName());
                                                                        if (toFind != args.entity) {
                                                                                for (List<String> locationList : locationsMightBeJoined) {
                                                                                        Location loc = DataUtils.generateLocationAndFillMap(locationList, args.aliasMap);
                                                                                        if (!args.toJoin.containsKey(loc)) {
                                                                                                args.toJoin.put(loc, args.aliasMap.get(loc));
                                                                                        }
                                                                                }
                                                                        }
                                                                        return DataUtils.generateLocationAndFillMap(list, args.aliasMap);
                                                                } else {
                                                                        if (!alreadySearched.contains(o)) {
                                                                                alreadySearched.add(o);
                                                                                objectStringMap.put(o, f.getName());
                                                                        }
                                                                }
                                                        }
                                                } else {
                                                        List<String> tmp = new ArrayList<String>(list);
                                                        tmp.add(f.getName());
                                                        locationsMightBeJoined.add(new ArrayList<String>(tmp));
                                                        // is plain object
                                                        if (obj == toFind) {
                                                                list.add(f.getName());
                                                                if (toFind != args.entity) {
                                                                        for (List<String> locationList : locationsMightBeJoined) {
                                                                                Location loc = DataUtils.generateLocationAndFillMap(locationList, args.aliasMap);
                                                                                if (!args.toJoin.containsKey(loc)) {
                                                                                        args.toJoin.put(loc, args.aliasMap.get(loc));
                                                                                }
                                                                        }
                                                                }
                                                                return DataUtils.generateLocationAndFillMap(list, args.aliasMap);
                                                        } else {
                                                                if (!alreadySearched.contains(obj)) {
                                                                        alreadySearched.add(obj);
                                                                        objectStringMap.put(obj, f.getName());
                                                                }
                                                        }
                                                }
                                        }
                                }
                        }
                        // not found
                        for (Object obj : objectStringMap.keySet()) {
                                List<String> nextList = new ArrayList<String>(list);
                                nextList.add(objectStringMap.get(obj));
                                List<List<String>> nextLocationsMightBeJoined = new ArrayList<List<String>>(locationsMightBeJoined);
                                Location location = findEntity(obj, toFind, nextList, args, alreadySearched, nextLocationsMightBeJoined);
                                if (null != location) {
                                        // found
                                        return location;
                                }
                        }
                        return null;
                } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                }
        }

        /**
         * 获取Entity位置,若没有找到则抛出异常
         *
         * @param toFind 要寻找的实体
         * @param args   上下文
         * @return 实体的位置(Location对象)
         */
        static Location findEntity(Object toFind, JPQLContext args) {
                if (toFind == args.entity) {
                        return new Location(null);
                } else {
                        Location l = findEntity(args.entity, toFind, new ArrayList<String>(), args, new HashSet<Object>(), new ArrayList<List<String>>());
                        if (l == null) {
                                throw new IllegalArgumentException("Cannot find location of " + toFind);
                        }
                        return l;
                }
        }
}
